{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "# 6.4. 多输入多输出通道\n",
    ":label:`sec_channels`\n",
    "\n",
    "虽然我们在 :numref:`subsec_why-conv-channels` 中描述了构成每个图像的多个通道和多层卷积层。例如彩色图像具有标准的 RGB 通道来指示红、绿和蓝。\n",
    "但是到目前为止，我们仅展示了单个输入和单个输出通道的简化例子。\n",
    "这使得我们可以将输入、卷积核和输出看作二维张量。\n",
    "\n",
    "当我们添加通道时，我们的输入和隐藏的表示都变成了三维张量。例如，每个RGB输入图像具有 $3\\times h\\times w$ 的形状。我们将这个大小为 $3$ 的轴称为 *通道*（channel） 维度。在本节中，我们将更深入地研究具有多输入和多输出通道的卷积核。\n",
    "\n",
    "\n",
    "## 6.4.1. 多输入通道\n",
    "\n",
    "当输入包含多个通道时，需要构造一个与输入数据具有相同输入通道数目的卷积核，以便与输入数据进行互相关运算。假设输入的通道数为 $c_i$，那么卷积核的输入通道数也需要为 $c_i$ 。如果卷积核的窗口形状是 $k_h\\times k_w$，那么当 $c_i=1$ 时，我们可以把卷积核看作形状为 $k_h\\times k_w$ 的二维张量。\n",
    "\n",
    "然而，当 $c_i>1$ 时，我们卷积核的每个输入通道将包含形状为 $k_h\\times k_w$ 的张量。将这些张量 $c_i$ 连结在一起可以得到形状为 $c_i\\times k_h\\times k_w$ 的卷积核。由于输入和卷积核都有 $c_i$ 个通道，我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算，再对通道求和（将 $c_i$ 的结果相加）得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。\n",
    "\n",
    "在 :numref:`fig_conv_multi_in` 中，我们演示了一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素：$(1\\times1+2\\times2+4\\times3+5\\times4)+(0\\times0+1\\times1+3\\times2+4\\times3)=56$。\n",
    "\n",
    "![两个输入通道的互相关计算。](../img/conv-multi-in.svg)\n",
    ":label:`fig_conv_multi_in`\n",
    "\n",
    "为了加深理解，我们将(**实现一下多输入通道互相关运算**)。\n",
    "简而言之，我们所做的就是对每个通道执行互相关操作，然后将结果相加。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "import paddle\n",
    "\n",
    "def corr2d(X, K):\n",
    "    \"\"\"2D相关计算\"\"\"\n",
    "    h, w = K.shape    \n",
    "    Y = paddle.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))\n",
    "    for i in range(Y.shape[0]):\n",
    "        for j in range(Y.shape[1]):\n",
    "            Y[i, j] = (X[i: i + h, j: j + w] * K).sum()\n",
    "    return Y\n",
    "\n",
    "\n",
    "def corr2d_multi_in(X, K):\n",
    "    # 沿着X和K的第0维（通道维）分别计算再相加\n",
    "    res = corr2d(X[0, :, :], K[0, :, :])\n",
    "    for i in range(1, X.shape[0]):\n",
    "        res += corr2d(X[i, :, :], K[i, :, :])\n",
    "    return res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Tensor(shape=[2, 2], dtype=float32, place=CPUPlace, stop_gradient=True,\n",
       "       [[56. , 72. ],\n",
       "        [104., 120.]])"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = paddle.to_tensor([[[0, 1, 2], \n",
    "                       [3, 4, 5], \n",
    "                       [6, 7, 8]],\n",
    "                      [[1, 2, 3], \n",
    "                       [4, 5, 6], \n",
    "                       [7, 8, 9]]], dtype='float32')\n",
    "K = paddle.to_tensor([[[0, 1], \n",
    "                       [2, 3]], \n",
    "                      [[1, 2], \n",
    "                       [3, 4]]], dtype='float32')\n",
    "\n",
    "# print(X.shape, K.shape)\n",
    "corr2d_multi_in(X, K)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "## 6.4.2. 多输出通道\n",
    "\n",
    "到目前为止，不论有多少输入通道，我们还只有一个输出通道。然而，正如我们在 :numref:`subsec_why-conv-channels` 中所讨论的，每一层有多个输出通道是至关重要的。在最流行的神经网络架构中，随着神经网络层数的加深，我们常会增加输出通道的维数，通过减少空间分辨率以获得更大的通道深度。直观地说，我们可以将每个通道看作是对不同特征的响应。而现实可能更为复杂一些，因为每个通道不是独立学习的，而是为了共同使用而优化的。因此，多输出通道并不仅是学习多个单通道的检测器。\n",
    "\n",
    "用 $c_i$ 和 $c_o$ 分别表示输入和输出通道的数目，并让 $k_h$ 和 $k_w$ 为卷积核的高度和宽度。为了获得多个通道的输出，我们可以为每个输出通道创建一个形状为 $c_i\\times k_h\\times k_w$ 的卷积核张量，这样卷积核的形状是 $c_o\\times c_i\\times k_h\\times k_w$。在互相关运算中，每个输出通道先获取所有输入通道，再以对应该输出通道的卷积核计算出结果。\n",
    "\n",
    "如下所示，我们实现一个[**计算多个通道的输出的互相关函数**]。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def corr2d_multi_in_out(X, K):\n",
    "    # 对K的第0维遍历，每次同输入X做互相关计算。所有结果使用stack函数合并在一起\n",
    "    return paddle.stack([corr2d_multi_in(X, k) for k in K])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "我们将核数组`K`同`K+1`（`K`中每个元素加一）和`K+2`连结在一起来构造一个输出通道数为3的卷积核。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[3, 2, 2, 2]"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "K = paddle.stack([K, K + 1, K + 2])\n",
    "K.shape # [3, 2, 2, 2]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "下面我们对输入数组`X`与核数组`K`做互相关运算。此时的输出含有3个通道。其中第一个通道的结果与之前输入数组`X`与多输入通道、单输出通道核的计算结果一致。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Tensor(shape=[3, 2, 2], dtype=float32, place=CPUPlace, stop_gradient=True,\n",
       "       [[[56. , 72. ],\n",
       "         [104., 120.]],\n",
       "\n",
       "        [[76. , 100.],\n",
       "         [148., 172.]],\n",
       "\n",
       "        [[96. , 128.],\n",
       "         [192., 224.]]])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "corr2d_multi_in_out(X, K)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "## 6.4.3. 卷积层\n",
    "\n",
    "[~~1x1卷积~~]\n",
    "$1 \\times 1 $卷积，即 $k_h = k_w = 1$，看起来似乎没有多大意义。毕竟，卷积的本质是有效提取相邻像素间的相关特征，而 $1 \\times 1$ 卷积显然没有此作用。\n",
    "尽管如此，$1 \\times 1$ 仍然十分流行，时常包含在复杂深层网络的设计中。下面，让我们详细地解读一下它的实际作用。\n",
    "\n",
    "因为使用了最小窗口，$1\\times 1$ 卷积失去了卷积层的特有能力——在高度和宽度维度上，识别相邻元素间相互作用的能力。\n",
    "而 $1\\times 1$ 卷积的唯一计算发生在通道上。\n",
    "\n",
    ":numref:`fig_conv_1x1` 展示了使用 $1\\times 1$ 卷积核与 $3$ 个输入通道和 $2$ 个输出通道的互相关计算。\n",
    "这里输入和输出具有相同的高度和宽度，输出中的每个元素都是从输入图像中同一位置的元素的线性组合。\n",
    "我们可以将 $1\\times 1$ 卷积层看作是在每个像素位置应用的全连接层，以 $c_i$ 个输入值转换为 $c_o$ 个输出值。\n",
    "因为这仍然是一个卷积层，所以跨像素的权重是一致的。\n",
    "同时，$1\\times 1$ 卷积层需要的权重维度为 $c_o\\times c_i$ ，再额外加上一个偏置。\n",
    "\n",
    "\n",
    "![互相关计算使用了具有3个输入通道和2个输出通道的 $1\\times 1$ 卷积核。其中，输入和输出具有相同的高度和宽度。](../img/conv-1x1.svg)\n",
    ":label:`fig_conv_1x1`\n",
    "\n",
    "下面，我们使用全连接层实现 $1 \\times 1$ 卷积。\n",
    "请注意，我们需要对输入和输出的数据形状进行微调。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def corr2d_multi_in_out_1x1(X, K):\n",
    "    c_i, h, w = X.shape\n",
    "    c_o = K.shape[0]\n",
    "    X = paddle.reshape(X, (c_i, h * w))\n",
    "    K = paddle.reshape(K, (c_o, c_i))\n",
    "    Y = paddle.matmul(K, X)  # 全连接层的矩阵乘法\n",
    "    return paddle.reshape(Y, (c_o, h, w))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": false
   },
   "source": [
    "经验证，做$1\\times 1$卷积时，以上函数与之前实现的互相关运算函数`corr2d_multi_in_out`等价。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = paddle.rand(shape=[3, 3, 3])\n",
    "K = paddle.rand(shape=[2, 3, 1, 1])\n",
    "\n",
    "Y1 = corr2d_multi_in_out_1x1(X, K)\n",
    "Y2 = corr2d_multi_in_out(X, K)\n",
    "\n",
    "(Y1 - Y2).norm().numpy()[0] < 1e-6"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## 6.4.4. 小结\n",
    "\n",
    "* 多输入多输出通道可以用来扩展卷积层的模型。\n",
    "* 当以每像素为基础应用时，$1\\times 1$ 卷积层相当于全连接层。\n",
    "* $1\\times 1$ 卷积层通常用于调整网络层的通道数量和控制模型复杂性。\n",
    "\n",
    "## 6.4.5. 练习\n",
    "\n",
    "1. 假设我们有两个卷积核，大小分别为 $k_1$ 和 $k_2$（中间没有非线性激活函数）。\n",
    "    1. 证明运算可以用单次卷积来表示。\n",
    "    1. 这个等效的单卷积的维数是多少呢？\n",
    "    1. 反之亦然吗？\n",
    "1. 假设输入为 $c_i\\times h\\times w$，卷积核大小为 $c_o\\times c_i\\times k_h\\times k_w$，填充为 $(p_h, p_w)$，步幅为 $(s_h, s_w)$。\n",
    "    1. 正向传播的计算成本（乘法和加法）是多少？\n",
    "    1. 内存占用是多少？\n",
    "    1. 反向传播的内存占用是多少？\n",
    "    1. 反向传播的计算成本是多少？\n",
    "1. 如果我们将输入通道 $c_i$ 和输出通道 $c_o$ 的数量加倍，计算数量会增加多少？如果我们把填充数量翻一番会怎么样？\n",
    "1. 如果卷积核的高度和宽度是 $k_h=k_w=1$，前向传播的计算复杂度是多少？\n",
    "1. 本节最后一个示例中的变量 `Y1` 和 `Y2` 是否完全相同？为什么？\n",
    "1. 当卷积窗口不是 $1\\times 1$ 时，如何使用矩阵乘法实现卷积？\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "PaddlePaddle 2.1.2 (Python 3.5)",
   "language": "python",
   "name": "py35-paddle1.2.0"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
